﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel.Web;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using GmaWebService.Interfaces;
using GmaWebService.Code.MediaInfoCode;
using MPWebStream.MediaTranscoding;

namespace GmaWebService.Code
{
    internal class Streaming
    {
        private const bool FFMPEG_DEBUG_MODE = false;
        private const int FAIL_OFFSET = -8; //if partial video copy fails, we use x times offset to cut from another position

        #region Data classes
        /// <summary>
        /// WCF recreates our object on every request, while we need some way to preserve objects between requests: the
        /// streamer process needs to be available on the status methods and on the end methods. This is a very ugly
        /// implementation that allows us to keep some data available in between requests.
        /// </summary>
        private static class PersistentHolder
        {
            public static Dictionary<string, ActiveStream> Streams = new Dictionary<string, ActiveStream>();
            public static Dictionary<string, WebMediaInfo> MediaInfoCache = new Dictionary<string, WebMediaInfo>();
        }

        public class ActiveStream
        {
            public string Identifier { get; set; }
            public string ClientDescription { get; set; }
            public FullTranscoderProfile Profile { get; set; }
            public string OriginalFile { get; set; }
            public string SourceFile { get; set; }
            public string BackgroundCopyFile { get; set; }
            public Resolution OutputSize { get; set; }
            public string FFmpegParametersTemplate { get; set; }
            public TranscodingStreamer CurrentTranscoder { get; set; }
            public Stream OutputStream { get; set; }
            public Thread PreprocessThread { get; set; }
            public string TemporaryDirectory { get; set; }
            public Thread ParseOutputThread { get; set; }
            public FFmpegEncodingInfo TranscodingInfo;
            public bool IsSeekReady { get; set; }

            public bool BackgroundCopyInProgress { get; set; }

            public bool IsSeekPreparing { get; set; }
        }

        // I miss multiple inheritance here; so emulate it.
        public class FullTranscoderProfile : TranscoderProfile
        {
            public int MaxOutputWidth { get; set; }
            public int MaxOutputHeight { get; set; }
            public string Description { get; set; }
            public string CodecParameters { get; set; }
            public string Target { get; set; }
            public string Bandwidth { get; set; }

            public FullTranscoderProfile()
            {
            }

            // this should be done better... I just don't know how (yet?)
            public FullTranscoderProfile(TranscoderProfile profile)
            {
                this.InputMethod = profile.InputMethod;
                this.MIME = profile.MIME;
                this.Name = profile.Name;
                this.OutputMethod = profile.OutputMethod;
                this.Parameters = profile.Parameters;
                this.Transcoder = profile.Transcoder;
                this.UseTranscoding = profile.UseTranscoding;

            }

            public WebTranscoderProfile ToWebTranscoderProfile()
            {
                return new WebTranscoderProfile()
                {
                    Name = this.Name,
                    Description = this.Description,
                    MIME = this.MIME,
                    MaxOutputWidth = this.MaxOutputWidth,
                    MaxOutputHeight = this.MaxOutputHeight,
                    UseTranscoding = this.UseTranscoding,
                    Target = this.Target,
                    Bandwidth = this.Bandwidth
                };
            }
        }

        [Flags]
        private enum CodecSeekType
        {
            Fastseek = 1,
            Slowseek = 2,
            BackgroundCopy = 4,
            SameContainer = 8,
            TSContainer = 16,
            PreConversion = 32,
            H264Mp4ToAnnexb = 64,
            BackgroundCopyFull = 128
        }

        /// <summary>
        /// Returns information about the codec. We used to have a few workarounds (background copy, etc.)
        /// which depended on the code type (not used anymore)
        /// </summary>
        /// <param name="extension"></param>
        /// <returns></returns>
        private CodecSeekType GetCodecSeekType(string extension)
        {
            return CodecSeekType.Fastseek;
        }
        #endregion

        #region Actual streaming
        /// <summary>
        /// Initialises a new stream.
        /// 
        /// Here's how the streaming currently works: InitStream is only called once for a streaming
        /// session. 
        /// 
        /// If the source file doesn't support partial copying (cutting parts of the video)
        /// a pre-conversion is started that copies the content of the file into a new container 
        /// (e.g. mkv -> ts). In that case seeking is disabled for the video until this pre-conversion
        /// is finished. From that time on all transcoding is done on the new container file. When starting
        /// from position 0, we still use the original file so to not interfere with the background 
        /// conversion.
        /// </summary>
        /// <param name="identifier">A unique identifier for this stream</param>
        /// <param name="clientDescription">A textual description of the streaming client</param>
        /// <param name="source">The source file</param>
        /// <returns></returns>
        public bool InitStream(string identifier, string clientDescription, string source)
        {
            if (!File.Exists(source))
            {
                Log.Error("Requested stream for non-existing file " + source);
                return false;
            }

            try
            {
                ActiveStream stream = new ActiveStream();
                stream.Identifier = identifier;
                stream.ClientDescription = clientDescription;
                stream.OriginalFile = source;
                stream.SourceFile = source;
                stream.TemporaryDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())).FullName;


                FileInfo sourceFile = new FileInfo(stream.SourceFile);
                CodecSeekType type = GetCodecSeekType(sourceFile.Extension);

                if (type.HasFlag(CodecSeekType.PreConversion))
                {
                    FullTranscoderProfile internalProfile = GetTranscoderProfileByName("Pre-Conversion");
                    if (internalProfile != null)
                    {
                        String newExtension = ".ts";
                        String newSource = Path.Combine(stream.TemporaryDirectory, "pre-conversion-" + identifier + newExtension);

                        stream.PreprocessThread = new Thread(new ThreadStart(delegate()
                        {
                            //do the pre-conversion
                            Process proc = new Process();
                            try
                            {
                                stream.BackgroundCopyInProgress = true;
                                stream.IsSeekPreparing = true;


                                Log.Info("Started muxing to pre-conversion ts file (since " + source + " doesn't support partial copying)");
                                stream.IsSeekReady = false;//can't seek until mux is done

                                string arguments = String.Format("-y -i \"{0}\" " + internalProfile.CodecParameters + " {1}", stream.SourceFile, newSource);
                                Log.Debug("FFMPEG COMMAND: " + internalProfile.Transcoder + " " + arguments);
                                ProcessStartInfo start = new ProcessStartInfo(internalProfile.Transcoder, arguments);
                                start.UseShellExecute = false;
                                start.RedirectStandardError = true;
                                FFmpegEncodingInfo info = new FFmpegEncodingInfo();

                                proc.StartInfo = start;
                                proc.Start();

                                Thread readerThread = new Thread(new ThreadStart(delegate()
                                    {
                                        FFmpegUtil.ParseOutputStream(proc.StandardError.BaseStream, out info, true, true);
                                    }
                                ));
                                readerThread.Start();


                                //stream.PreprocessThread 

                                proc.WaitForExit();

                                if (!info.FinishedSuccessfully && info.EncodingErrors.HasFlag(FFMpegEncodingErrors.NonMonotonicallyIncreasingDts))
                                {
                                    //we can work around this by converting the audio stream
                                    Log.Info("Pre-conversion failed due to faulty audio codec, trying to convert to different audio codec");
                                    arguments = arguments.Replace("-acodec copy", "-acodec ac3");
                                    Log.Debug("FFMPEG COMMAND: " + internalProfile.Transcoder + " " + arguments);
                                    start = new ProcessStartInfo(internalProfile.Transcoder, arguments);
                                    start.UseShellExecute = false;
                                    start.RedirectStandardError = true;

                                    proc.StartInfo = start;
                                    proc.Start();

                                    readerThread = new Thread(new ThreadStart(delegate()
                                        {
                                            FFmpegUtil.ParseOutputStream(proc.StandardError.BaseStream, out info, true, FFMPEG_DEBUG_MODE);
                                        }
                                    ));
                                    readerThread.Start();

                                    proc.WaitForExit();
                                }

                                //TODO: check if ffmpeg finished successfully instead of relying on MediaInfo (needs output parsing)
                                //long originalDuration = getDuration(stream.OriginalFile);
                                //long duration = getDuration(newSource);

                                //Log.Info("Duration after pre-conversion: " + duration + " | " + originalDuration);

                                if (info.FinishedSuccessfully)
                                {
                                    stream.SourceFile = newSource;//from now on, use the new (remuxed) file as source
                                    stream.IsSeekReady = true;

                                    Log.Info("Finished muxing to pre-conversion ts file");
                                }
                                else
                                {
                                    Log.Info("Muxing to pre-conversion ts file failed");
                                }

                                stream.IsSeekPreparing = false;
                                stream.BackgroundCopyInProgress = false;

                            }
                            catch (ThreadAbortException)
                            {
                                if (proc != null && !proc.HasExited)
                                    proc.Kill();
                            }
                            catch (Exception ex)
                            {
                                Log.Error("Failed to do the muxing to pre-conversion ts file", ex);
                            }
                        }));

                        stream.PreprocessThread.Priority = ThreadPriority.Lowest;
                        stream.PreprocessThread.Start();

                        Thread.Sleep(FFMPEG_DEBUG_MODE ? 5000 : 500);
                    }
                    else
                    {
                        Log.Warn("No internal pre-conversion profile defined, seeking not available for this stream");
                    }
                }
                else
                {
                    stream.IsSeekReady = true;
                }


                // and save it
                PersistentHolder.Streams[identifier] = stream;

                return true;
            }
            catch (Exception ex)
            {
                Log.Error("Failure during InitStream", ex);
                return false;
            }
        }

        private long getDuration(string _path)
        {
            if (File.Exists(_path))
            {
                MediaInfo originalFileInfo = new MediaInfo();
                originalFileInfo.Option("ParseSpeed", "0.3");
                originalFileInfo.Open(_path);
                long originalDuration;
                Int64.TryParse(originalFileInfo.Get(StreamKind.Video, 0, "Duration"), out originalDuration);
                originalFileInfo.Close();
                return originalDuration;
            }
            return 0;
        }

        public bool StartStream(string identifier, FullTranscoderProfile profile, int position = 0)
        {
            try
            {
                if (profile == null)
                {
                    Log.Error("StartStream called with invalid profile");
                    return false;
                }

                if (!PersistentHolder.Streams.ContainsKey(identifier))
                {
                    Log.Error("StartStream called with invalid identifier");
                    return false;
                }

                ActiveStream stream = PersistentHolder.Streams[identifier];
                stream.Profile = profile;

                if (!stream.IsSeekReady && position != 0)
                {
                    Log.Error("StartStream called with position " + position + ", but seeking not (yet) available for this video");
                    return false;
                }

                // get the aspect ratio and calculate the height / width
                WebMediaInfo info = GetMediaInfo(stream.SourceFile);
                // apparantly, H264 requires the frame size to be a multiple of 2
                stream.OutputSize = Resolution.Calculate(info.DisplayAspectRatio, new Resolution(profile.MaxOutputWidth, profile.MaxOutputHeight), 2);

                // create ffmpeg command line
                stream.FFmpegParametersTemplate = String.Format(
                    "-y #IN# -s {0} -aspect {1} {2} #OUT#",
                    stream.OutputSize, info.DisplayAspectRatio.ToString("00.#####", System.Globalization.CultureInfo.InvariantCulture), profile.CodecParameters);

                bool success = false;
                for (int i = 0; i < 10; i++)
                {
                    int offset = i * FAIL_OFFSET;
                    if (position + offset < 0)
                    {
                        //in case the offset would put use < 0, set offset so we start at
                        //excactly 0
                        offset = position * -1;
                    }


                    // generate parameters
                    string newSource = stream.SourceFile;
                    string inputParameters = "";
                    FileInfo sourceFile = new FileInfo(stream.SourceFile);
                    CodecSeekType type = GetCodecSeekType(sourceFile.Extension);
                    FFmpegEncodingInfo partialCopyEncodingInfo = null;
                    if (position == 0 || !type.HasFlag(CodecSeekType.BackgroundCopy))
                    {
                        // we can use original file without a background copy
                        if (type.HasFlag(CodecSeekType.Fastseek) && position != 0)
                            inputParameters += String.Format("-ss {0} ", position);
                        inputParameters += "-i \"{0}\"";
                        if (type.HasFlag(CodecSeekType.Slowseek) && position != 0)
                            inputParameters += String.Format(" -ss {0}", position);
                    }
                    else
                    {
                        // Transcoding from a given position: Since ffmpeg has problems with fastseek (directly jumping
                        // to position in video) we first use ffmpeg to copy everything from "position" to a temporary
                        // file and do the encoding with this new file. We usually use the input container here, but since
                        // some containers suck and aren't streamable at all or behave really weird, these are blacklisted
                        // above and replaced with an mpeg ts container, which gives all other kinds of problems. Really 
                        // painful this part. 
                        FullTranscoderProfile copyProfile = GetTranscoderProfileByName("Partial-Copy-Conversion");
                        if (copyProfile != null)
                        {
                            inputParameters += "-i \"{0}\"";

                            if (offset != 0)
                            {
                                inputParameters += " -ss " + offset * -1;
                            }

                            string newExtension = type.HasFlag(CodecSeekType.SameContainer) ? sourceFile.Extension : ".ts";
                            newSource = Path.Combine(stream.TemporaryDirectory, "copy-from-" + (position + offset) + newExtension);
                            stream.BackgroundCopyFile = newSource;
                            stream.BackgroundCopyInProgress = true;

                            Process proc = new Process();
                            stream.PreprocessThread = new Thread(new ThreadStart(delegate()
                            {
                                
                                try
                                {
                                    Log.Info("Started muxing to temporary seek file (" + newSource + " doesn't support ffmpeg fastseek)");
                                    // FIXME: we drop subtitles here
                                    string seek = type.HasFlag(CodecSeekType.Fastseek) ? "-ss {1} -i \"{0}\" " : "-i \"{0}\" -ss {1} ";

                                    string arguments = String.Format("-y " + seek + copyProfile.CodecParameters + " \"{2}\"", stream.SourceFile, position + offset, newSource);
                                    if (type.HasFlag(CodecSeekType.H264Mp4ToAnnexb))
                                    {
                                        arguments = arguments.Replace("-vcodec copy", "-vcodec copy -vbsf h264_mp4toannexb");
                                    }

                                    Log.Debug("FFMPEG COMMAND: " + stream.Profile.Transcoder + " " + arguments);
                                    ProcessStartInfo start = new ProcessStartInfo(stream.Profile.Transcoder, arguments);
                                    start.UseShellExecute = false;
                                    start.RedirectStandardError = true;

                                    proc.StartInfo = start;
                                    proc.Start();

                                    Thread readerThread = new Thread(new ThreadStart(delegate()
                                        {
                                            FFmpegUtil.ParseOutputStream(proc.StandardError.BaseStream, out partialCopyEncodingInfo, true, FFMPEG_DEBUG_MODE);
                                        }
                                    ));
                                    readerThread.Start();

                                    proc.WaitForExit();

                                    if (!partialCopyEncodingInfo.FinishedSuccessfully && partialCopyEncodingInfo.EncodingErrors.HasFlag(FFMpegEncodingErrors.NonMonotonicallyIncreasingDts))
                                    {
                                        //we can work around this by converting the audio stream
                                        Log.Info("Partial-copy failed due to faulty audio codec, trying to convert to different audio codec");
                                        arguments = arguments.Replace("-acodec copy", "-acodec ac3");
                                        Log.Debug("FFMPEG COMMAND: " + stream.Profile.Transcoder + " " + arguments);
                                        start = new ProcessStartInfo(stream.Profile.Transcoder, arguments);
                                        start.UseShellExecute = false;
                                        start.RedirectStandardError = true;

                                        proc.StartInfo = start;
                                        proc.Start();

                                        readerThread = new Thread(new ThreadStart(delegate()
                                        {
                                            FFmpegUtil.ParseOutputStream(proc.StandardError.BaseStream, out partialCopyEncodingInfo, true, FFMPEG_DEBUG_MODE);
                                        }
                                        ));
                                        readerThread.Start();

                                        proc.WaitForExit();
                                    }

                                    stream.BackgroundCopyInProgress = false;
                                    Log.Info("Finished muxing to temporary seek file");
                                }
                                catch (ThreadAbortException)
                                {
                                    stream.BackgroundCopyInProgress = false;
                                    if (proc != null && !proc.HasExited)
                                        proc.Kill();
                                }
                                catch (Exception ex)
                                {
                                    stream.BackgroundCopyInProgress = false;
                                    Log.Error("Failed to do the background copy", ex);
                                }
                            }));
                            stream.PreprocessThread.Start();

                            bool isReady = false;
                            while (!isReady && stream.BackgroundCopyInProgress && stream.PreprocessThread.IsAlive)
                            {
                                //TODO: instead of using MediaInfo, use the parsed output of ffmpeg (not yet available)
                                Thread.Sleep(50);
                                Log.Trace("Partial copy position: " + (partialCopyEncodingInfo != null ? partialCopyEncodingInfo.CurrentTime : 0));

                                if (partialCopyEncodingInfo != null
                                     && !type.HasFlag(CodecSeekType.BackgroundCopyFull) 
                                     && partialCopyEncodingInfo.CurrentTime > 60)
                                {
                                    isReady = true;
                                    stream.IsSeekReady = true;
                                }
                            }
                            Thread.Sleep(FFMPEG_DEBUG_MODE ? 5000 : 0); // make sure some data is already processed
                        }
                        else
                        {
                            Log.Error("Fast-seek not available for " + stream.SourceFile + ", but internal profile for partial copy not found");
                        }
                    }


                    if (partialCopyEncodingInfo == null || (partialCopyEncodingInfo.CurrentTime > 60))
                    {
                        stream.Profile.Parameters = stream.FFmpegParametersTemplate.Replace("#IN#", inputParameters).Replace("#OUT#", "\"{1}\"");
                        // normally MPWebStream does this but it can't handle WCF, so do it ourselfs
                        //WebOperationContext.Current.OutgoingResponse.ContentType = stream.Profile.MIME;
                        Log.Info("Starting transcoding on " + newSource);
                        Log.Trace("Trace is enabled for diagnosting ffmpeg output");
                        // do the streaming
                        Log.Debug("FFMPEG COMMAND: " + stream.Profile.Transcoder + " " + String.Format(stream.Profile.Parameters, newSource, "out.ts"));
                        stream.CurrentTranscoder = new TranscodingStreamer(newSource, stream.Profile);
                        stream.CurrentTranscoder.WantLogStream = true;
                        stream.CurrentTranscoder.StartTranscoding();
                        Log.Debug("StartTranscoding() returned");

                        // setup output parsing
                        stream.TranscodingInfo = new FFmpegEncodingInfo();

                        Thread transcodingParserThread = new Thread(new ThreadStart(delegate()
                        {
                            FFmpegUtil.ParseOutputStream(stream.CurrentTranscoder.GetTranscoderLogStream(), out stream.TranscodingInfo, true, true);
                        }
                        ));
                        transcodingParserThread.Start();

                        // start the stream
                        stream.OutputStream = stream.CurrentTranscoder.StartStream();
                        Log.Debug("StartStream() returned");

                        // wait till stream is ready (but break when the transcoder is dead)
                        while (!stream.OutputStream.CanRead && stream.CurrentTranscoder.IsTranscoding)
                        {
                            System.Threading.Thread.Sleep(50);
                        }
                        Log.Debug("Output ready");

                        Thread.Sleep(FFMPEG_DEBUG_MODE ? 5000 : 500); // make sure some data is already processed
                        success = stream.CurrentTranscoder.IsTranscoding;

                        //TODO: check for a) ffmpeg crashed and b) lot's of probes 
                        //Log.Error("Transcoding failed for " + position + ", trying with offset " + ((i + 1) * FAIL_OFFSET));
                    }
                    else
                    {
                        success = false;
                        Log.Warn("Partial-copy ended, but didn't produce any results (maybe ffmpeg crashed)");
                    }

                    if (success)
                    {
                        break;
                    }
                    else
                    {
                        Log.Error("Transcoding failed for " + position + ", trying with offset " + ((i + 1) * FAIL_OFFSET));
                        //Stop the current attempt and try again with offset
                        EndStream(identifier);
                    }
                }
                return success;
            }
            catch (Exception ex)
            {
                Log.Error("Error during StartStream", ex);
                return false;
            }
        }

        public Stream RetrieveStream(string identifier)
        {
            if (PersistentHolder.Streams != null && PersistentHolder.Streams.ContainsKey(identifier))
            {
                return PersistentHolder.Streams[identifier].OutputStream;
            }
            else
            {
                return null;
            }
        }

        public void EndStream(string identifier)
        {
            if (PersistentHolder.Streams != null && PersistentHolder.Streams.ContainsKey(identifier) && PersistentHolder.Streams[identifier].CurrentTranscoder != null)
            {
                ActiveStream stream = PersistentHolder.Streams[identifier];
                if (stream != null && stream.PreprocessThread != null && stream.PreprocessThread.IsAlive)
                {
                    Log.Info("Removing preprocess transcoding thread (background-copy)");
                    stream.PreprocessThread.Abort();
                    while (stream.PreprocessThread.IsAlive)
                        Thread.Sleep(50);

                }
                stream.CurrentTranscoder.StopTranscoding();
                stream.CurrentTranscoder = null;

                if (stream.BackgroundCopyFile != null)
                {
                    DeleteFileDelayed(stream.BackgroundCopyFile);
                    stream.BackgroundCopyFile = null;
                }
            }
        }

        private void DeleteFileDelayed(string _file)
        {
            if (_file != null)
            {
                Log.Info("Deleting temporary file " + _file);
                Thread deleteThread = new Thread(new ThreadStart(delegate()
                        {
                            bool deleted = false;
                            for (int i = 0; i < 5; i++)
                            {
                                try
                                {
                                    Thread.Sleep(5000);
                                    if (!File.Exists(_file))
                                    {
                                        break;
                                    }
                                    //delete the temporary copy file
                                    File.Delete(_file);
                                    deleted = true;

                                }
                                catch (Exception ex)
                                {
                                    Log.Warn("Couldn't deleting temporary copy file (attempt " + i + ")", ex);
                                }
                            }

                            if (!deleted)
                            {
                                Log.Error("Failed to delete file " + _file);
                            }
                        }));
                deleteThread.Start();
            }
        }

        public void KillStream(string identifier)
        {
            EndStream(identifier);

            if (PersistentHolder.Streams != null && PersistentHolder.Streams.ContainsKey(identifier))
            {
                ActiveStream stream = PersistentHolder.Streams[identifier];
                if (stream != null && stream.PreprocessThread != null && stream.PreprocessThread.IsAlive)
                {
                    stream.PreprocessThread.Abort();
                    while (stream.PreprocessThread.IsAlive)
                        Thread.Sleep(50);
                }
                try
                {
                    Directory.Delete(PersistentHolder.Streams[identifier].TemporaryDirectory, true);
                }
                catch (IOException e)
                {
                    Log.Error("Failed to remove temporary directory", e);
                }
                PersistentHolder.Streams.Remove(identifier);
            }
        }

        internal List<WebStreamingSession> GetStreamingSessions()
        {
            return PersistentHolder.Streams.
                Select(kvp => kvp.Value).
                Select(s => new WebStreamingSession(s.Identifier, s.ClientDescription, s.Profile.Name, s.SourceFile, s.TranscodingInfo.ToWebTranscodingInfo())).ToList();
        }
        #endregion

        #region Profiles
        public FullTranscoderProfile GetTranscoderProfileByName(string name)
        {
            return GetTranscoderProfiles().Where(s => s.Name == name).FirstOrDefault();
        }

        public List<FullTranscoderProfile> GetTranscoderProfiles()
        {
            // maybe this belongs in Util? or should be rewritten using Linq2XML?
            List<FullTranscoderProfile> list = new List<FullTranscoderProfile>();
            XmlDocument doc = new XmlDocument();
            doc.Load(AppDomain.CurrentDomain.BaseDirectory + "config.xml");
            XmlNodeList nodes = doc.SelectNodes("/appconfig/transcoders/transcoder");
            foreach (XmlNode node in nodes)
            {
                FullTranscoderProfile profile = new FullTranscoderProfile(TranscoderProfile.CreateFromXmlNode(node));
                profile.MaxOutputWidth = Int32.Parse(node.ChildNodes.Cast<XmlNode>().Where(n => n.Name == "maxOutputWidth").FirstOrDefault().InnerText);
                profile.MaxOutputHeight = Int32.Parse(node.ChildNodes.Cast<XmlNode>().Where(n => n.Name == "maxOutputHeight").FirstOrDefault().InnerText);
                profile.Description = node.ChildNodes.Cast<XmlNode>().Where(n => n.Name == "description").FirstOrDefault().InnerText;
                profile.CodecParameters = node.ChildNodes.Cast<XmlNode>().Where(n => n.Name == "codecParameters").FirstOrDefault().InnerText;
                profile.Target = node.ChildNodes.Cast<XmlNode>().Where(n => n.Name == "target").FirstOrDefault().InnerText;
                profile.Bandwidth = node.ChildNodes.Cast<XmlNode>().Where(n => n.Name == "bandwidth").FirstOrDefault().InnerText;

                list.Add(profile);
            }
            return list;
        }
        #endregion

        #region Info
        public WebMediaInfo GetMediaInfo(string source)
        {
            // first check for cache hit
            if (PersistentHolder.MediaInfoCache.ContainsKey(source))
                return PersistentHolder.MediaInfoCache[source];

            if (!File.Exists(source))
            {
                Log.Warn("GetMediaInfo: File " + source + " doesn't exist or is not accessible");
                return null;
            }

            /* Loosely based upon MediaInfoWrapper.cs (mediaportal/Core/Player) from MediaPortal trunk r27491 as of 15 June 2011
             * 
             * Using the whole wrapper from MediaPortal is quite much porting work as it's cluttered with calls to other MP code. Referencing
             * MediaPortal.Core.dll also isn't an option as that pulls in a big tree of dependencies. 
             * 
             * TODO: Needs error handling
             * TODO: No support for DVDs yet
             * TODO: Aspect ratio doesn't work properly yet
             */
            MediaInfo info = new MediaInfo();
            info.Option("ParseSpeed", "0.3");
            info.Open(source);

            // why can't properties be passed as out parameters?
            long duration;
            Int64.TryParse(info.Get(StreamKind.Video, 0, "Duration"), out duration);
            string aspectString = info.Get(StreamKind.Video, 0, "DisplayAspectRatio/String");
            string strAspect = info.Get(StreamKind.Video, 0, "DisplayAspectRatio");
            decimal aspect = 0;
            if (strAspect != null && !strAspect.Equals(""))
            {
                aspect = Decimal.Parse(strAspect, System.Globalization.CultureInfo.InvariantCulture);
            }
            string codec = info.Get(StreamKind.Video, 0, "Codec/String");

            info.Close();

            CodecSeekType type = GetCodecSeekType(new FileInfo(source).Extension);
            bool streamNeedsPreconversion = type.HasFlag(CodecSeekType.PreConversion);

            // save it in cache
            WebMediaInfo ret = new WebMediaInfo()
            {
                Duration = duration,
                DisplayAspectRatioString = aspectString,
                DisplayAspectRatio = aspect,
                SourceCodec = codec,
                StreamingNeedsPreconversion = streamNeedsPreconversion
            };
            PersistentHolder.MediaInfoCache[source] = ret;
            return ret;
        }

        public WebTranscodingInfo GetTranscodingInfo(string identifier)
        {
            if (PersistentHolder.Streams.ContainsKey(identifier))
            {
                ActiveStream stream = PersistentHolder.Streams[identifier];
                WebTranscodingInfo info = stream.TranscodingInfo.ToWebTranscodingInfo();
                info.SeekAvailable = stream.IsSeekReady;
                info.SeekPreparing = stream.IsSeekPreparing;
                return info;
            }
            return null;
        }
        #endregion

        #region Image extraction
        public Stream ExtractImage(string source, int startPosition, int? maxWidth, int? maxHeight)
        {
            if (!File.Exists(source))
            {
                Log.Warn("ExtractImage: File " + source + " doesn't exist or is not accessible");
                return null;
            }
            // calculate size
            string ffmpegResize = "";
            if (maxWidth != null && maxHeight != null)
            {
                ffmpegResize = "-s " + Resolution.Calculate(GetMediaInfo(source).DisplayAspectRatio, new Resolution(maxWidth.Value, maxHeight.Value)).ToString();
            }

            // execute it
            string tempFile = Path.GetTempFileName(); // FIXME: maybe we need to clean this up?
            ProcessStartInfo info = new ProcessStartInfo();
            info.Arguments = String.Format("-ss {0} -vframes 1 -i \"{1}\" {2} -f image2 {3}", startPosition, source, ffmpegResize, tempFile);
            info.FileName = GetTranscoderProfiles().First().Transcoder;
            info.UseShellExecute = false;
            Process proc = new Process();
            proc.StartInfo = info;
            proc.Start();
            proc.WaitForExit();

            return new FileStream(tempFile, FileMode.Open, FileAccess.Read);
        }
        #endregion
    }
}
